﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FitNesseRunner;
using FitNesseRunner.Model;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace FitNesseRunnerTests
{
	[TestClass]
	public class ScriptTableHandlerShould
	{
		private FakeReporter _fakeReporter;
		private FakeReflectionProvider _reflectionProvider;
		private FakeContext _fakeContext;
		private ScriptTableHandler _scriptTableHandler;

		[TestInitialize]
		public void Setup()
		{
			_fakeReporter = new FakeReporter();
			_reflectionProvider = new FakeReflectionProvider();
			_fakeContext = new FakeContext(_reflectionProvider);
			_scriptTableHandler = new ScriptTableHandler(_fakeReporter, _fakeContext);
		}

		[TestMethod]
		public void InstantiateClass()
		{
			var instance = new object();
			_reflectionProvider.SetConstructor("ClassName", args => instance);
			var scriptTable = new FakeTable(new[] {"script", "class name"});
			_scriptTableHandler.Handle(scriptTable);

			Assert.AreSame(instance, _reflectionProvider.InstantiatedObject);
		}

		[TestMethod]
		public void PassArgumentsToConstructor()
		{
			var instance = new object();
			_reflectionProvider.SetConstructor("ClassName", args =>
			                                                	{
			                                                		Assert.AreEqual("arg1", args.ElementAt(0));
			                                                		Assert.AreEqual("arg2", args.ElementAt(1));
			                                                		Assert.AreEqual("arg3", args.ElementAt(2));
			                                                		return instance;
			                                                	});

			var scriptTable = new FakeTable(new[] { "script", "class name", "arg1", "arg2", "arg3" });
			_scriptTableHandler.Handle(scriptTable);

			Assert.AreSame(instance, _reflectionProvider.InstantiatedObject);
		}

		[TestMethod]
		public void TrimConstructorArguments()
		{
			var instance = new object();
			_reflectionProvider.SetConstructor("ClassName", args =>
			{
				Assert.AreEqual("arg1", args.ElementAt(0));
				Assert.AreEqual("arg2", args.ElementAt(1));
				Assert.AreEqual("arg3", args.ElementAt(2));
				return instance;
			});

			var scriptTable = new FakeTable(new[] { "script", "class name", " arg1  ", "  arg2  ", "  arg3  " });
			_scriptTableHandler.Handle(scriptTable);

			Assert.AreSame(instance, _reflectionProvider.InstantiatedObject);
		}

		[TestMethod]
		public void ExecuteMethods()
		{
			var scriptTable = new FakeTable(new[] {"script", "test script"},
			                                new[] {"method1"},
			                                new[] {"method2"});
			_scriptTableHandler.Handle(scriptTable);
			
			CollectionAssertEx.AreEqual(new[] {"Method1", "Method2"}, _reflectionProvider.CalledMethods);
		}

		[TestMethod]
		public void ExecuteMethodWithOneArgument()
		{
			var scriptTable = new FakeTable(new[] {"script", "test script"},
			                                new[] {"Method1", "arg1"});

			_reflectionProvider.AddMethod("Method1", (object arg) =>
			                                         	{
			                                         		Assert.AreEqual("arg1", arg);
			                                         		return null;
			                                         	});
			_scriptTableHandler.Handle(scriptTable);

			CollectionAssertEx.AreEqual(new[] { "Method1" }, _reflectionProvider.CalledMethods);
		}

		[TestMethod]
		public void ExecuteMethodWithMultipleArguments()
		{
			var scriptTable = new FakeTable(new[] { "script", "test script" },
											new[] { "Method1", "arg1", "And", "arg2", "AndAlso", "arg3"});

			_reflectionProvider.AddMethod("Method1", args =>
			                                         	{
			                                         		Assert.AreEqual("arg1", args.ElementAt(0));
			                                         		Assert.AreEqual("arg2", args.ElementAt(1));
			                                         		Assert.AreEqual("arg3", args.ElementAt(2));
			                                         	});
			_scriptTableHandler.Handle(scriptTable);

			CollectionAssertEx.AreEqual(new[] { "Method1AndAndAlso" }, _reflectionProvider.CalledMethods);
		}

		[TestMethod]
		public void HandleCheckKeywordWithSuccess()
		{
			_reflectionProvider.AddMethod("Method1", (object o) => "expected");
			var scriptTable = new FakeTable(new[] { "script", "test script" },
											new[] { "check", "method1", "expected" });

			_scriptTableHandler.Handle(scriptTable);
			Assert.AreEqual(1, _fakeReporter.PassMessages.Count);
			Assert.AreEqual(0, _fakeReporter.FailedMessages.Count);
			Assert.AreEqual("Method1==expected", _fakeReporter.PassMessages.Single());
		}

		[TestMethod]
		public void HandleCheckKeywordWithFailure()
		{
			_reflectionProvider.AddMethod("Method1", (object o) => "not expected");
			var scriptTable = new FakeTable(new[] { "script", "test script" },
											new[] { "check", "method1", "expected" });

			_scriptTableHandler.Handle(scriptTable);
			Assert.AreEqual(0, _fakeReporter.PassMessages.Count);
			Assert.AreEqual(1, _fakeReporter.FailedMessages.Count);
		}

		[TestMethod]
		public void HandleRejectKeywordWithSuccess()
		{
			_reflectionProvider.AddMethod("Method1", (object o) => false);
			var scriptTable = new FakeTable(new[] { "script", "test script" },
											new[] { "reject", "Method1" });

			_scriptTableHandler.Handle(scriptTable);
			Assert.AreEqual(1, _fakeReporter.PassMessages.Count);
			Assert.AreEqual(0, _fakeReporter.FailedMessages.Count);
			Assert.AreEqual("Method1 is false", _fakeReporter.PassMessages.Single());
		}

		[TestMethod]
		public void HandleRejectKeywordWithFailure()
		{
			_reflectionProvider.AddMethod("Method1", (object o) => true);
			var scriptTable = new FakeTable(new[] { "script", "test script" },
											new[] { "reject", "Method1" });

			_scriptTableHandler.Handle(scriptTable);
			Assert.AreEqual(0, _fakeReporter.PassMessages.Count);
			Assert.AreEqual(1, _fakeReporter.FailedMessages.Count);
			var failedMessage = _fakeReporter.FailedMessages.Single();
			Assert.AreEqual("Method1", failedMessage.MethodName);
			Assert.AreEqual(false, failedMessage.ExpectedValue);
			Assert.AreEqual(true, failedMessage.ActualValue);
		}

		[TestMethod]
		public void HandleTableVariables()
		{
			var scriptTable = new FakeTable(new[] {"script", "test script"},
			                                new[] {"$x=", "method1", ""},
			                                new[] {"method2", "$x"});

			_reflectionProvider.AddMethod("Method1", (object p) => "Result of Method1");
			_reflectionProvider.AddMethod("Method2", (object p) => Assert.AreEqual("Result of Method1", p));

			_scriptTableHandler.Handle(scriptTable);

			CollectionAssertEx.AreEqual(new[] {"Method1", "Method2"}, _reflectionProvider.CalledMethods);
		}

		[TestMethod]
		public void UnassignedVariablesResolveToNull()
		{
			var scriptTable = new FakeTable(new[] { "script", "test script" },
											new[] { "method1", "$x" });

			_reflectionProvider.AddMethod("Method1", 
				(object p) => Assert.AreEqual(string.Empty, p, "$x is expected to be empty, but was: {0}", p));

			_scriptTableHandler.Handle(scriptTable);

			CollectionAssertEx.AreEqual(new[] { "Method1" }, _reflectionProvider.CalledMethods);
		}

		[TestMethod]
		public void HandleReassigningToAVariable()
		{
			var scriptTable = new FakeTable(new[] {"script", "test script"},
			                                new[] {"$x=", "method1", ""},
			                                new[] {"method2", "$x"},
			                                new[] {"$x=", "method3"},
			                                new[] {"method4", "$x"});

			_reflectionProvider.AddMethod("Method1", (object p) => "Result of Method1");
			_reflectionProvider.AddMethod("Method2", (object p) => Assert.AreEqual("Result of Method1", p));
			_reflectionProvider.AddMethod("Method3", (object p) => "Result of Method3");
			_reflectionProvider.AddMethod("Method4", (object p) => Assert.AreEqual("Result of Method3", p));

			_scriptTableHandler.Handle(scriptTable);

			CollectionAssertEx.AreEqual(new[] { "Method1", "Method2", "Method3", "Method4" }, _reflectionProvider.CalledMethods);
		}

		[TestMethod]
		public void NumbersAreNotResovledAsVariables()
		{
			var scriptTable = new FakeTable(new[] { "script", "test script" },
											new[] { "Method1", "$1.23" });

			_reflectionProvider.AddMethod("Method1", (object p) => Assert.AreEqual("$1.23", p));

			_scriptTableHandler.Handle(scriptTable);

			CollectionAssertEx.AreEqual(new[] { "Method1" }, _reflectionProvider.CalledMethods);
		}

		[TestMethod]
		public void VariablesCanBePassedBetweenTables()
		{
			var scriptTable1 = new FakeTable(new[] { "script", "test script1" },
											new[] { "$x=", "method1", "" });

			var scriptTable2 = new FakeTable(new[] { "script", "test script2" },
											new[] { "method2", "$x" });

			_reflectionProvider.AddMethod("Method1", (object p) => "Result of Method1");
			_reflectionProvider.AddMethod("Method2", (object p) => Assert.AreEqual("Result of Method1", p));

			_scriptTableHandler.Handle(scriptTable1);
			_scriptTableHandler.Handle(scriptTable2);

			CollectionAssertEx.AreEqual(new[] { "Method1", "Method2" }, _reflectionProvider.CalledMethods);
		}

		public class FakeContext : IContext
		{
			private readonly IReflectionProvider _reflectionProvider;
			private readonly Dictionary<string, string> _variables = new Dictionary<string, string>();

			public FakeContext(IReflectionProvider reflectionProvider)
			{
				_reflectionProvider = reflectionProvider;
			}

			public object LastScriptTableObject
			{
				get { return null; }
				set { }
			}

			public IReflectionProvider ReflectionProvider
			{
				get { return _reflectionProvider; }
			}

			public IDictionary<string, string> Variables
			{
				get { return _variables; }
			}

			public void AddImports(IEnumerable<string> imports)
			{
				throw new NotImplementedException();
			}
		}

		public class FakeReporter : IReporter
		{
			public FakeReporter()
			{
				PassMessages = new List<string>();
				FailedMessages = new List<FailedMessage>();
			}
		
			public void Pass(string format, params object[] args)
			{
				PassMessages.Add(string.Format(format, args));
			}

			public void Invoking(string className, string methodName, IEnumerable<string> parameterValues)
			{
			}

			public void InvocationFailed(string className, string methodName, IEnumerable<string> parameterValues, Exception exception)
			{
				var message = string.Format("Invocation failed for {0}.{1}({2})", className, methodName, string.Join(", ", parameterValues));
				throw new Exception(message, exception);
			}

			public void Constructing(string className)
			{
			}

			public void Failed(object instance, string methodName, object expectedValue, object actualValue)
			{
				FailedMessages.Add(new FailedMessage(instance, methodName, expectedValue, actualValue));
			}

			public void RunTest(string path)
			{
				throw new NotImplementedException();
			}

			public void FailedToConstruct(string className, Exception ex)
			{
				throw new NotImplementedException();
			}

			public void Invoking(PropertyInfo propertyInfo, string stringValue)
			{
				throw new NotImplementedException();
			}

			public void Show(string methodName, string result)
			{
				throw new NotImplementedException();
			}

			public void TestError(string message)
			{
				throw new NotImplementedException();
			}

			public void InvocationError(string message, Type objectType, string query, IEnumerable<string> parameters)
			{
				throw new NotImplementedException();
			}

			public List<string> PassMessages { get; private set; }

			public List<FailedMessage> FailedMessages { get; private set; }

			public class FailedMessage
			{
				public object Instance { get; set; }

				public FailedMessage(object instance, string methodName, object expectedValue, object actualValue)
				{
					Instance = instance;
					MethodName = methodName;
					ExpectedValue = expectedValue;
					ActualValue = actualValue;
				}

				public string MethodName { get; private set; }

				public object ExpectedValue { get; private set; }

				public object ActualValue { get; private set; }
			}
		}
	}
}
